#include "JSIExecutorTest.h"

#include <gtest/gtest.h>

#include <variant>

#include "test_host/TestHostFunc.h"
#include "test_host/TestHostObj.h"
#include "test_native/MyNativeModule.h"

TEST_P(JSIExecutorTest, TestBytecode) {
#ifdef VM_TYPE
  #if VM_TYPE == JSC
  logInfo("skipping Bytecode test for now, will try jsc bytecode test\n");
  #elif VM_TYPE == QJS
  testing::internal::CaptureStdout();
  const uint8_t qjsc_sayhello[81] = {
      0x02, 0x04, 0x0e, 0x63, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x06, 0x6c, 0x6f, 0x67, 0x16, 0x68, 0x65,
      0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x16, 0x73, 0x61, 0x79, 0x68, 0x65, 0x6c, 0x6c,
      0x6f, 0x2e, 0x6a, 0x73, 0x0e, 0x00, 0x06, 0x00, 0xa0, 0x01, 0x00, 0x01, 0x00, 0x03, 0x00, 0x00, 0x14,
      0x01, 0xa2, 0x01, 0x00, 0x00, 0x00, 0x38, 0xe1, 0x00, 0x00, 0x00, 0x42, 0xe2, 0x00, 0x00, 0x00, 0x04,
      0xe3, 0x00, 0x00, 0x00, 0x24, 0x01, 0x00, 0xcd, 0x28, 0xc8, 0x03, 0x01, 0x00,
  };

  std::string script(reinterpret_cast<const char*>(qjsc_sayhello), 81);
  jsiexecutor::JSIExecutor quickjsExecutor(std::move(runtime), nullptr);
  quickjsExecutor.loadBundle(std::make_unique<facebook::jsi::StringBuffer>(script), "test");
  std::string output = testing::internal::GetCapturedStdout();
  EXPECT_EQ(output, "hello world\n");
  #endif
#endif
}

#if 0
TEST_P(JSIExecutorTest, TestStdFile) {
  testing::internal::CaptureStdout();
  jsiexecutor::JSIExecutor quickjsExecutor(std::move(runtime), nullptr);

  std::string script = R"(
      import * as std from "std";
      try{
        const file = std.open("test.txt", "w");
        file.puts("Hello, World!");
        file.close();
        const contents = std.loadFile("test.txt");
        console.log(contents);
      }catch(e){
        console.log(e);
      }
    )";

  quickjsExecutor.loadBundle(std::make_unique<facebook::jsi::StringBuffer>(script), "test");
  std::string output = testing::internal::GetCapturedStdout();
  EXPECT_EQ(output, "");
}
#endif

TEST_P(JSIExecutorTest, TestJs) {
  testing::internal::CaptureStdout();
  std::string json = R"({"x":1, "y":2})";

  jsiexecutor::JSIExecutor jsiExecutor(std::move(runtime), nullptr);

  jsiExecutor.setGlobalVariable("$data", json);

  std::string script = R"(
    (function(){
      globalThis = new Function("return this")();
      function $print(msg){
          console.log(msg);
      }
      globalThis.$print=$print;
      var $data = globalThis.$data;
      console.log($data);
      var $expression = [
          ()=> console.log($data.x),
          ()=> $data.y,
          ()=> console.log("abc"),
          (a, b)=> console.log(a + b)
      ];
      globalThis.$expression = $expression;
    })();
    globalThis.$print("hello");
    )";

  jsiExecutor.loadBundle(std::make_unique<facebook::jsi::StringBuffer>(script), "test");
  auto res = jsiExecutor.callFunction("$print", 1234);
  auto exp = jsiExecutor.callFunction(0);
  jsiExecutor.callFunction(3, 3, 3);
  std::string output = testing::internal::GetCapturedStdout();
  EXPECT_EQ(output, "[object Object]\nhello\n1234\n1\n6\n");
}

#if 0
TEST_P(JSIExecutorTest, TestHostFuncObj) {
  testing::internal::CaptureStdout();
  jsiexecutor::JSIExecutor quickjsExecutor(std::move(runtime), nullptr);
  quickjsExecutor.setGlobalHostFunc(
      "testFunc",
      [testFunc = std::move(test_host_func::testFunction)](facebook::jsi::Runtime& runtime,
                                                            const facebook::jsi::Value&,
                                                            const facebook::jsi::Value* args, size_t count) {
        testFunc(args[0].asNumber());
        return facebook::jsi::Value::undefined();
      },
      1);
  // can't handle unknown # of args case (see testSum func def), the result(sum) here is wrong
  quickjsExecutor.setGlobalHostFunc(
      "testSum",
      [testSum = std::move(test_host_func::testSum)](facebook::jsi::Runtime& runtime, const facebook::jsi::Value&,
                                                      const facebook::jsi::Value* args, size_t count) {
        testSum(args[0].asNumber(), args[1].asNumber());
        return facebook::jsi::Value::undefined();
      },
      2);

  std::string script = R"(
      console.log("test host func");
      console.log(this);
      this.testFunc(2);
      this.testSum(1, 3);
      console.log("sum should be 3, but actually a random number");
    )";

  quickjsExecutor.loadBundle(std::make_unique<facebook::jsi::StringBuffer>(script), "test");
  std::string output = testing::internal::GetCapturedStdout();
  EXPECT_EQ(output, "test host func\n[object Object]\nin testFunction: 2\nin testSum: 1\nidx: 0, "
    "val1784660896\n sum: 1784660896\nsum should be 3, but actually a random number\n");
}
#endif

TEST_P(JSIExecutorTest, TestHostObject) {
  testing::internal::CaptureStdout();
  auto host_obj = std::make_shared<test_host_obj::TestHostObject>();
  facebook::jsi::Object obj = facebook::jsi::Object::createFromHostObject(*runtime, host_obj);
  const std::string code = R"(
      (function(obj){
          console.log('in eval');
          //console.log('obj: ' + obj);
          console.log('initial, obj.x: ' + obj.x);
          obj.x = 24566;
          console.log('after set, obj.x: ' + obj.x);
          Object.keys(obj).forEach((prop)=>{
            console.log(prop, obj[prop]);
          });
      })
    )";
  auto args = {facebook::jsi::Value(*runtime, facebook::jsi::String::createFromUtf8(*runtime, code))};
  facebook::jsi::Function eval_func = runtime->global().getPropertyAsFunction(*runtime, "eval");
  facebook::jsi::Value val = eval_func.call(*runtime, args);
  val.getObject(*runtime).getFunction(*runtime).call(*runtime, {facebook::jsi::Value(*runtime, obj)});
  std::string output = testing::internal::GetCapturedStdout();
  EXPECT_EQ(output, "in eval\ninitial, obj.x: 0\ndone set in TestHostObject\nafter set, obj.x: 24566\nx 24566\ny \n");
}

TEST_P(JSIExecutorTest, TestHostFunctionException) {
#ifdef VM_TYPE
  #if VM_TYPE == JSC
  logInfo("skipping HostFunctionException test, jsc holds different error msg.\n");
  #elif VM_TYPE == QJS
  testing::internal::CaptureStderr();
  unsigned callsBeforeError = 5;

  facebook::jsi::Function sometimesThrows = function(
      "function sometimesThrows(shouldThrow, callback) {"
      "  if (shouldThrow) {"
      "    throw Error('Omg, what a nasty exception')"
      "  }"
      "  callback(callback);"
      "}");

  facebook::jsi::Function callback = facebook::jsi::Function::createFromHostFunction(
      *runtime, facebook::jsi::PropNameID::forAscii(*runtime, "callback"), 0,
      [&sometimesThrows, &callsBeforeError](facebook::jsi::Runtime& rt, const facebook::jsi::Value& thisVal,
                                            const facebook::jsi::Value* args, size_t count) {
        return sometimesThrows.call(rt, --callsBeforeError == 0, args[0]);
      });

  try {
    sometimesThrows.call(*runtime, true, callback);
    throw std::exception();
  } catch (facebook::jsi::JSError& error) {
    logError("catch JSError\n");
  } catch (...) {
    logError("catch ...\n");
  }
  std::string output = testing::internal::GetCapturedStderr();
  EXPECT_EQ(output,
            "checkAndThrowException error: Omg, what a nasty exception, "
            "stack:     at sometimesThrows (<input>)\n\ncatch JSError\n");
  #endif
#endif
}

TEST_P(JSIExecutorTest, TestCurrying) {
  testing::internal::CaptureStdout();
  std::string json = R"({"x":1, "y":2})";

  jsiexecutor::JSIExecutor jsiExecutor(std::move(runtime), nullptr);

  jsiExecutor.setGlobalVariable("$data", json);

  std::string script = R"(
    (function(){
      globalThis = new Function("return this")();
      function sayhello(msg, index) {
        console.log(msg + ' ' + index);
      }

      function commonCurrying(msg, index, curringFunc) {
        return function() {
          curringFunc(msg, index);
        };
      }

        globalThis.commonCurrying = commonCurrying;
        globalThis.sayhello = sayhello;

      var $data = globalThis.$data;
      console.log($data);
      var $expression = [
          ()=> console.log($data.x),
          ()=> $data.y,
          ()=> console.log("abc"),
          (a)=> console.log(4 + a),
          ()=> sayhello
      ];
      globalThis.$expression = $expression;
    })();
    globalThis.sayhello("hello", 0);
    var func = globalThis.commonCurrying("hello", 1, globalThis.sayhello);
    func();
    )";

  jsiExecutor.loadBundle(std::make_unique<facebook::jsi::StringBuffer>(script), "test");

  // currying function create
  std::vector<std::string> pictures = {"cat", "dog", "tiger"};
  for (size_t i = 0; i < pictures.size(); ++i) {
    logInfo("picture: " + pictures[i] + "\n");

    auto globalThis = jsiExecutor.getGlobalThis();
    auto func = globalThis.getPropertyAsFunction(*jsiExecutor.getRuntime(), "commonCurrying");

    auto sayhello =
        jsiExecutor.callFunction(4).getObject(*jsiExecutor.getRuntime()).getFunction(*jsiExecutor.getRuntime());

    auto curryingFunc = func.call(*jsiExecutor.getRuntime(), "hello", (int)i, sayhello);
    curryingFunc.getObject(*jsiExecutor.getRuntime())
        .getFunction(*jsiExecutor.getRuntime())
        .call(*jsiExecutor.getRuntime(), {});
  }
  std::string output = testing::internal::GetCapturedStdout();
  EXPECT_EQ(
      output,
      "[object Object]\nhello 0\nhello 1\npicture: cat\nhello 0\npicture: dog\nhello 1\npicture: tiger\nhello 2\n");
}

TEST_P(JSIExecutorTest, TestRegex) {
  jsiexecutor::JSIExecutor jsiExecutor(std::move(runtime), nullptr);
  std::string script = R"(
      (function(){
        "use strict";
        globalThis = new Function("return this")();
        globalThis.$regex = Object.create(Object.prototype, {
          exec : {
            value: function(expression, text){
              return expression.exec(text);
            }
          }
        });

        console.log("test evaluate js");
      })();
    )";
  jsiExecutor.loadBundle(std::make_unique<facebook::jsi::StringBuffer>(script), "test");

  auto regexScript = std::make_shared<facebook::jsi::StringBuffer>(R"(
    var expression = 'hello world';
    var regex = /hello/;
    $regex.exec(regex, expression);
  )");
  auto result = jsiExecutor.getRuntime()->evaluateJavaScript(regexScript, "regex");
  EXPECT_TRUE(result.isObject());
  auto objResult = result.getObject(*jsiExecutor.getRuntime());
  EXPECT_TRUE(objResult.isArray(*jsiExecutor.getRuntime()));
  auto arrayResult = objResult.getArray(*jsiExecutor.getRuntime());
  EXPECT_EQ(arrayResult.size(*jsiExecutor.getRuntime()), 1);
  auto value = arrayResult.getValueAtIndex(*jsiExecutor.getRuntime(), 0);
  EXPECT_TRUE(value.isString());
  auto strValue = value.getString(*jsiExecutor.getRuntime());
  EXPECT_EQ(strValue.utf8(*jsiExecutor.getRuntime()), "hello");
}

INSTANTIATE_TEST_SUITE_P(Runtimes, JSIExecutorTest, ::testing::ValuesIn(runtimeGenerators()));